package db

import "encoding/binary"
import "bytes"
import "fmt"
import "time"
import "strconv"
import "github.com/boltdb/bolt"

// i64tob converts an integer into a BigEndian []byte representation.  This allows
// a bytewise comparison to sort by numeric value, provided they are all non-negative.
func i64tob(i int64) []byte {
	b := make([]byte, 0, 8)
	buf := bytes.NewBuffer(b)
	binary.Write(buf, binary.BigEndian, i)
	return buf.Bytes()[:8]
}

// btoi64 does the inverse of i64tob.
func btoi64(b []byte) int64 {
	var i int64
	binary.Read(bytes.NewReader(b), binary.BigEndian, &i)
	return i
}

// i64stob converts n int64s found in data to BigEndian []byte representation.
// data is an interface so that it can be any type that under the hood is an []int64.
func i64stob(n int, data interface{}) []byte {
	b := make([]byte, 0, n*8)
	buf := bytes.NewBuffer(b)
	binary.Write(buf, binary.BigEndian, data)
	return buf.Bytes()[:n*8]
}

// btoi64 does the inverse of i64stob: it converts int64s represented in BigEndian
// in b into int64s in data.
func btoi64s(b []byte, data interface{}) {
	binary.Read(bytes.NewReader(b), binary.BigEndian, data)
}

// timetob converts a time.Time into a []byte representation.
func timetob(t time.Time) []byte {
	return i64tob(t.UnixNano())
}

// btotime converts to time.Time from a []byte representation of epoch millis, panicing on parse failure.
func btotime(val []byte) time.Time {
	return time.Unix(0, btoi64(val))
}

// nidsToCsv encodes nums as a string of comma-separated decimal values.
func nidsToCsv(nums []nid) string {
	var buf bytes.Buffer
	for _, n := range nums {
		buf.Write(strconv.AppendInt([]byte{}, int64(n), 10))
		buf.WriteByte(',')
	}
	if buf.Len() == 0 {
		return ""
	}
	// Return all but the trailing comma
	return string(buf.Bytes()[:buf.Len()-1])
}

type BucketSource interface {
	Bucket(name []byte) *bolt.Bucket
}

type BucketSink interface {
	BucketSource
	DeleteBucket(name []byte) (error)
	CreateBucket(name []byte) (*bolt.Bucket, error)
	CreateBucketIfNotExists(name []byte) (*bolt.Bucket, error)
}

func OpenBuckets(bsrc BucketSource, names []string) ([]*bolt.Bucket, error) {
	bs := make([]*bolt.Bucket, len(names))
	for i, bname := range names {
		if b := bsrc.Bucket([]byte(bname)); b == nil {
			return nil, fmt.Errorf("can't open bucket '%s'", bname)
		} else {
			bs[i] = b
		}
	}
	return bs, nil
}

func CreateBucketsIfNotExists(bsnk BucketSink, buckets []string) ([]*bolt.Bucket, error) {
	bs := make([]*bolt.Bucket, len(buckets))
	for i, bname := range buckets {
		if b, err := bsnk.CreateBucketIfNotExists([]byte(bname)); err != nil {
			return nil, fmt.Errorf("can't make bucket '%s': %s", bname, err)
		} else {
			bs[i] = b
		}
	}
	return bs, nil
}

// bucketTx is a Bucket together with its name.  If the bucket does not exist
// err will be populated.
type bucketTx struct {
	*bolt.Bucket
	bucketName []byte
	err error
}

func openBucketTx(bsrc BucketSource, name string) *bucketTx {
	btx := &bucketTx{bucketName: []byte(name)}
	btx.open(bsrc)
	return btx
}

func bucketsToBucketTxs(bs []*bolt.Bucket, names []string) []*bucketTx {
	btxs := make([]*bucketTx, len(bs))
	for i, b := range bs {
		btxs[i] = &bucketTx{bucketName: []byte(names[i]), Bucket: b}
	}
	return btxs
}

func openBucketTxs(bsrc BucketSource, names []string) ([]*bucketTx, error) {
	bs, err := OpenBuckets(bsrc, names)
	if err != nil {
		return nil, err
	}
	return bucketsToBucketTxs(bs, names), nil
}

// create the given buckets within bsnk and return them, or error if any fail
func openOrCreateBucketTxs(bsnk BucketSink, names []string) ([]*bucketTx, error) {
	bs, err := CreateBucketsIfNotExists(bsnk, names)
	if err != nil {
		return nil, err
	}
	return bucketsToBucketTxs(bs, names), nil
}

func (btx *bucketTx) open(bsrc BucketSource) error {
	b := bsrc.Bucket(btx.bucketName)
	if b == nil {
		btx.err = btx.Errorf("does not exist")
	} else {
		btx.Bucket = b
	}
	return btx.err
}

func openOrCreateBucketTx(bsnk BucketSink, name string) *bucketTx {
	btx := &bucketTx{bucketName: []byte(name)}
	btx.openOrCreate(bsnk)
	return btx
}

// open or create the bucket
func (btx *bucketTx) openOrCreate(bsnk BucketSink) error {
	b, err := bsnk.CreateBucketIfNotExists(btx.bucketName)
	if err != nil {
		btx.err = btx.Errorf("error opening/creating: ", err)
	} else {
		btx.Bucket = b
	}
	return btx.err
}

func (btx *bucketTx) BucketName() string {
	return string(btx.bucketName)
}

func (btx *bucketTx) Errorf(format string, args ...interface{}) error {
	fmtstr := "[bucket " + btx.BucketName() + "] " + format
	return fmt.Errorf(fmtstr, args...)
}

// PutKeyString stores (key,value) in the provided bucket or return an error.
// The reason 'String' is in the method name is to warn the caller that the 
// key will be rendered as a string in the error.
func (btx *bucketTx) PutKeyString(key, value []byte) error {
	if btx.err != nil {
		return btx.err
	}
	if err := btx.Put(key, value); err != nil {
		return btx.Errorf("Put(key=%s) failed: %v", string(key), err)
	}
	return nil
}

// PutKeyInt64 stores (key,value) in the provided bucket or returns an error.
// key is converted to a []byte as a BigEndian int, thus it will sort properly.
func (btx *bucketTx) PutKeyInt64(key int64, value []byte) error {
	if btx.err != nil {
		return btx.err
	}
	keyb := i64tob(int64(key))
	if err := btx.Put(keyb, value); err != nil {
		return btx.Errorf("Put(key=%d) failed: %v", key, err)
	}
	return nil
}

